#!/usr/bin/env python3
# Author: Joshua Chen
# Date: 2015-12-05
# Location: Shenzhen
# Desc: secure data before pushing it to the remote,
# it pushes one branch at a time.

import os, sys
prog_path = os.path.realpath(__file__)
prog_dir  = os.path.dirname(prog_path)
sys.path.insert(0, prog_dir)
import lib

class Pusher():
    """ Find out all commits created since the last secure-push in the given
    branch; archive all objects of all the commits found, encrypt it and save
    it to a blob object, create a tree for it, then a commit; the file name,
    author info can be faked . Encrypted commit will be placed in a totally
    separated branch named like this: cipher-origin-master. After transforming
    all plaintext commits into ciphertext, call git-push to actually push.
    """

    def __init__(self, remote, branch, force=False):
        lc_p_branch          = 'plain-%s-%s' % (remote, branch)
        self.checkBranch(lc_p_branch)   # may raise an exception
        self.remote          = remote
        self.branch          = branch
        self.force           = force
        self.lc_c_branch     = 'cipher-%s-%s' % (remote, branch)
        self.rmt_c_branch    = 'cipher-%s' % branch
        position_record      = lib.get_position_record(remote, branch)
        self.plain_position  = position_record[0]
        self.cipher_position = position_record[1]
        self.new_objects     = []
        self.transformed     = False
        self.transform()

    def checkBranch(self, branch):
        """ If the branch exists, raise an exception
        """
        msg = "%s exists, push not allowed" % branch
        assert not lib.branch_exists(branch), msg

    def transform(self):
        """ Transform plaintext commits into one ciphertext commit, including
        all objects directly or indirectly referenced by the commits;
        create or update the cipher branch to point to the newly created
        cipher commit; encrypt the symmetric key and create a new tag for it.
        """
        commits = lib.find_all_commits(self.plain_position, self.branch)
        if not commits:
            print('Nothing to push')
            return
        parent = self.cipher_position
        name   = lib.dense_time_str()
        dname  = name
        fname  = name + '.txz.ci'
        key    = lib.generate_key()
        for commit in commits:
            lib.copy_out_objects(commit, topdir=dname)
        blob        = lib.encrypt_path(dname, key)
        tree        = lib.create_tree(blob, fname, base=parent)
        message     = 'CIPHER\n\nTree: %s\nBlob: %s\nTop: %s\nBot: %s\n' % (tree, blob, commits[-1], commits[0])
        new_commit  = lib.create_commit(tree, parent, message)
        tname       = lib.symkey_tag_prefix + tree
        key_blob    = lib.secure_key(key, tname)
        reason = "prepare for shadow-push"
        lib.update_branch(self.lc_c_branch, new_commit, reason)
        self.new_objects.extend([new_commit, tree, blob, key_blob])
        self.cipher_key_tag       = tname
        self.transformed          = True
        self.newest_plain_commit  = commits[-1]
        self.newest_cipher_commit = new_commit
        lib.cleanup(dname)

    def push(self):
        """ Push the transformed (encrypted) branch to the remote.
        On success, update the last-pushed record mapping for the plain
        and cipher; on failure, reset the cipher branch back to the last
        pushed, remove the tag of the symmetric key, remove all objects
        created thus far.
        """
        if not self.transformed: return True
        if lib.push(self.remote, self.lc_c_branch,
                self.rmt_c_branch, self.cipher_key_tag, self.force):
            lib.update_position_record(self.remote, self.branch,
                        self.newest_plain_commit,
                        self.newest_cipher_commit)
            return True
        else:
            print('Push failed, rolling back...', file=sys.stderr)
            reason = "shadow-push failed, roll back"
            lib.update_branch(self.lc_c_branch, self.cipher_position, reason)
            lib.remove_tag(self.cipher_key_tag)
            lib.prune_objects(self.new_objects)
            return False


def help(ofile=sys.stdout):
    """ Show help message
    """
    msg = 'Usage: %s [-f] remote branch' % os.path.basename(sys.argv[0])
    print(msg, file=ofile)


if __name__ == '__main__':
    if not lib.env_ok():
        exit(1)

    if len(sys.argv) < 3:
        help(sys.stderr)
        exit(1)

    if '-f' in sys.argv:
        force = True
        sys.argv.remove('-f')
    else:
        force = False
    remote, branch = sys.argv[1:3]
    try:
        pusher = Pusher(remote, branch, force)
        stat = pusher.push()
        code = 0 if stat else 1
        exit(code)
    except KeyboardInterrupt:
        exit(1)
    except Exception as e:
        print(str(e))
        exit(1)
